import pyparsing as p

single_quoted_string = p.QuotedString("'", '\\')
double_quoted_string = p.QuotedString('"', '\\')
numeral = p.Word(p.nums)
number = p.Combine(p.Optional(p.Literal('-')) + numeral + 
    p.Optional(p.Literal('.') + p.Optional(numeral)))
dee = p.Literal('d').suppress()
dice_roll = numeral + dee + numeral
dice_rolls = p.delimitedList(dice_roll | numeral, '+')
identifier = p.Word(p.alphas + '_')
_value = (single_quoted_string | double_quoted_string | 
    dice_rolls ^ number | identifier)
comma = p.Literal(',').suppress()
series = p.Group(_value + (p.OneOrMore(comma + _value) | comma))
value = series | _value
colon = p.Literal(':').suppress()
lbrack = p.Literal('[').suppress()
rbrack = p.Literal(']').suppress()

def _mark_place(s, l, tok):
    tok[0] = tok[0], s, l
    return tok
block = p.Group(lbrack + 
    identifier.setResultsName('key') + 
    p.Group(p.ZeroOrMore(colon + value)).setResultsName('value') + 
    rbrack).setResultsName('block').setParseAction(_mark_place)

leading_indent = p.Optional(p.White(' \t')).setResultsName('leading_indent')
line = leading_indent + p.Group(
    block + p.ZeroOrMore(~p.LineEnd() + block)).setResultsName('blocks')

def _fail_hard(s, loc, expr, err):
    raise p.ParseFatalException(s, loc, 
        'parse error before last left bracket')
file = (p.ZeroOrMore(
        p.SkipTo((leading_indent + lbrack).leaveWhitespace()).suppress() + 
        p.Group(line)) + 
    (~p.SkipTo(lbrack)).setFailAction(_fail_hard))

class Result(object):
    def __init__(self, name, value, **attrs):
        self.name = name
        self.value = value
        self.attrs = attrs
        self.children = []
    
    def add_child(self, _child=None, **attrs):
        if _child is None:
            _child = Result(**attrs)
        self.children.append(_child)
        return _child
    
    def __repr__(self):
        return '<Result %s:%r %r with %d children>' % (
            self.name, self.value, self.attrs, len(self.children))
    
    def __getitem__(self, key):
        return self.attrs[key]
    
    def __setitem__(self, key, value):
        self.attrs[key] = value
    
    def get(self, key, default=None):
        return self.attrs.get(key, default)

def parse_file(filename):
    ret = []
    stack = []
    
    def handle_dedent(target_level):
        level = None
        while stack and target_level <= stack[-1][0]:
            level, result = stack.pop()
            if stack:
                if result.children or result.attrs:
                    stack[-1][1].add_child(result)
                else:
                    stack[-1][1][result.name] = result.value
            else:
                ret.append(result)
        
        if level is not None and level != target_level:
            raise p.ParseException(_s, _l, 
                'unindent does not match any outer indentation level')

    level = last_level = -1
    last_count = 0
    for tok in file.parseFile(filename):
        last_level, level = level, len(tok.leading_indent)
        for block, _s, _l in tok.blocks:
            if last_level == -1 and level > 0:
                raise p.ParseException(_s, _l,
                    'indent with no root block')
            elif level > last_level and last_count > 1:
                raise p.ParseException(_s, _l, 'ambiguous indent')
            
            result = Result(block.key, block.value.asList())
            handle_dedent(level)
            stack.append((level, result))
        last_count = len(tok.blocks)
    handle_dedent(0)
    return ret
